package com.googlecode.afx.spring;

import java.util.Locale;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.ClassUtils;

import com.googlecode.afx.annotation.AFXController;
import com.googlecode.afx.annotation.AFXView;

/**
 * <code>BeanFactoryPostProcessor</code> responsible for initializing all annotated JavaFX components, like  controllers, derived 
 * from <code>AbstractController</code> and that are annotated with the <code>AFXController</code> annotation. 
 * 
 * @author MartinKoster
 *
 */
public class AFXComponentsBeanFactoryPostProcessor implements
		BeanFactoryPostProcessor, ApplicationContextAware {

	private static final Log LOG = LogFactory.getLog(AFXComponentsBeanFactoryPostProcessor.class);
	
	private static final String BEANNAME_JFXVIEWINITIALIZER = "jfxViewInitializer";

	private static final String BEANNAME_JFXCONTROLLERPOSTPROCESSOR = "jfxControllerPostProcessor";
	
	private String basePackage;
	
	private ApplicationContext applicationContext;
	
	private MessageSource messageSource;
	
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

		// register AFXViewInitializer and AFXControllerPostProcessor
		this.registerJFXProcessors(beanFactory);
		
		
		// scan for AFXController annotations...
		ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
		scanner.addIncludeFilter(new AnnotationTypeFilter(AFXController.class));
		Set<BeanDefinition> definitions = scanner.findCandidateComponents(this.basePackage == null ? "" : this.basePackage );
		if(definitions != null) {
			
			for(BeanDefinition definition : definitions) {
				
				// inspect class given in BeanDefiniton
				AFXController jfxController = this.extractJFXControllerAnnotation(definition);
				if(jfxController != null) {
					this.registerBeanDefinition(jfxController.id(), definition, beanFactory);
					
					// check for AFXView annotation
					AFXView jfxView = this.extractJFXViewAnnotation(definition);
					if(jfxView != null) {
						BeanDefinition jfxViewBeanDefinition = this.buildBeanDefinition(jfxView);
						this.registerBeanDefinition(jfxView.id(), jfxViewBeanDefinition, beanFactory);
					}
				}
			}
		}
	}
	
	/**
	 * Extracts the <code>AFXController</code> annotation from the class given in the <code>BeanDefinition</code>.
	 * If the annotation is not present on that class, <tt>null</tt> will be returned.
	 * 
	 * @param definition
	 * @return
	 */
	protected AFXController extractJFXControllerAnnotation(BeanDefinition definition) {
		return AnnotationUtils.findAnnotation(this.resolveClassName(definition), AFXController.class);
	}

	/**
	 * Extracts the <code>AFXView</code> annotation from the class given in the <code>BeanDefinition</code>.
	 * If the annotation is not present on that class, <tt>null</tt> will be returned.
	 * 
	 * @param definition
	 * @return
	 */
	protected AFXView extractJFXViewAnnotation(BeanDefinition definition) {
		return AnnotationUtils.findAnnotation(this.resolveClassName(definition), AFXView.class);
	}
	
	/**
	 * Resolves the class from a given bean definition.
	 * 
	 * @param definition
	 * @return
	 */
	protected Class<?> resolveClassName(BeanDefinition definition) {
		return ClassUtils.resolveClassName(definition.getBeanClassName(), ClassUtils.getDefaultClassLoader());	
	}
	
	/**
	 * Creates a <code>BeanDefinition</code> based on <code>AFXView</code> annotation.
	 * 
	 * @param jfxView
	 * @return
	 */
	protected BeanDefinition buildBeanDefinition(AFXView jfxView) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(AFXViewInitializer.resolveViewClass(jfxView));

		builder.addPropertyValue("id", jfxView.id());
		builder.addPropertyValue("title", jfxView.title());
		builder.addPropertyValue("sizeX", jfxView.sizeX());
		builder.addPropertyValue("sizeY", jfxView.sizeY());
		builder.addPropertyValue("modal", jfxView.modal());
		builder.addPropertyValue("maximized", jfxView.maximized());
		builder.addPropertyValue("parentId", jfxView.parentId());
		builder.addPropertyValue("position", jfxView.position());
		builder.addPropertyValue("fxml", jfxView.fxml());
		
		return builder.getBeanDefinition();
	}
	
	/**
	 * Registers the <code>JFXViewFactory</code> and <code>AFXControllerPostProcessor</code> with the Spring context.
	 */
	protected void registerJFXProcessors(ConfigurableListableBeanFactory beanFactory) {
		AFXViewInitializer  jfxViewInitializer = new AFXViewInitializer();
		jfxViewInitializer.setApplicationContext(applicationContext);
		jfxViewInitializer.setMessageSource(messageSource);
		jfxViewInitializer.setLocale(Locale.getDefault());
		this.registerSingleton(BEANNAME_JFXVIEWINITIALIZER, jfxViewInitializer, beanFactory);
		
		AFXControllerPostProcessor  jfxControllerPostProcessor = new AFXControllerPostProcessor();
		jfxControllerPostProcessor.setJfxViewInitializer(jfxViewInitializer);
		try {
			jfxControllerPostProcessor.afterPropertiesSet();
		}
		catch(Exception e) {
			throw new FatalBeanException("", e);
		}
		this.registerSingleton(BEANNAME_JFXCONTROLLERPOSTPROCESSOR, jfxControllerPostProcessor, beanFactory);
	}

	
	/**
	 * Registers the <code>BeanDefinition</code> with the Spring context.
	 */
	protected void registerBeanDefinition(String beanName, BeanDefinition definition, ConfigurableListableBeanFactory beanFactory) {
		BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) beanFactory;
		beanDefinitionRegistry.registerBeanDefinition(beanName, definition);
		LOG.info("Registered BeanDefinition of type '" + definition.getBeanClassName() + "' with bean name '" + beanName + "'.");
	}

	
	/**
	 * Registers the given bean under the provided name in the given bean factory.
	 * 
	 * @param name
	 * @param bean
	 * @param beanFactory
	 */
	protected void registerSingleton(String beanName, Object singletonObject, ConfigurableListableBeanFactory beanFactory) {
		beanFactory.registerSingleton(beanName, singletonObject);
		if(LOG.isInfoEnabled()) {
			LOG.info("Registered singleton instance of type '" + singletonObject.getClass().getName() + "' with bean name '"+ beanName + "'.");
		}
	}

	public String getBasePackage() {
		return basePackage;
	}

	public void setBasePackage(String basePackage) {
		this.basePackage = basePackage;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.applicationContext = applicationContext;
	}

	public MessageSource getMessageSource() {
		return messageSource;
	}

	public void setMessageSource(MessageSource messageSource) {
		this.messageSource = messageSource;
	}	
}
